package com.maschinenstuermer.profiler.model;

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Logger;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

import com.maschinenstuermer.profiler.model.impl.CompositeInvocation;
import com.maschinenstuermer.profiler.model.impl.Invocation;
import com.maschinenstuermer.profiler.model.impl.MethodInvocation;
import com.maschinenstuermer.profiler.model.impl.ProfiledMethod;
import com.maschinenstuermer.profiler.model.impl.SqlStatementExecution;
import com.maschinenstuermer.profiler.model.impl.ThreadCallStack;
import com.maschinenstuermer.profiler.model.impl.Trace;
import com.maschinenstuermer.profiler.model.impl.Transaction;
import com.maschinenstuermer.profiler.timer.TimerFactory;

public class TraceModelBuilder {
	private static Logger logger = Logger.getLogger(TraceModelBuilder.class.getName());
	
	private static Trace trace;
	
	private static final ConcurrentMap<Long, Stack<CompositeInvocation>> threadCallStack = 
		new ConcurrentHashMap<Long, Stack<CompositeInvocation>>();
	
	private static final ConcurrentMap<Long, SqlStatementExecution> threadExecutedSqlStatement = 
		new ConcurrentHashMap<Long, SqlStatementExecution>();
	
	private TraceModelBuilder() {
	}

	public static void profile(String methodName) {
		trace.addProfiledMethod(methodName);
	}
	
	public static ProfiledMethod getMethod(String methodName) {
		return trace.getMethod(methodName);
	}
	
	public static Trace startProfiling() {
		trace = new Trace();
		return trace;
	}
	
	public static Trace stopProfiling() {
		Trace result = trace;
		result.accept(new PostProcessModelVisitor());
		trace = new Trace();
		return result;
	}

	public static SqlStatementExecution executeStatement(String className, String statement) {
		SqlStatementExecution statementExecution = null;
		if (statement != null) {
			long startTickNs = TimerFactory.get().nanoTime();
		
			statementExecution = new SqlStatementExecution(className, statement);
			statementExecution.getMetrics().startOrContinue(startTickNs);
			Stack<CompositeInvocation> callStack = callStack();
			if (!callStack.isEmpty()) {
				CompositeInvocation activeInvocation = callStack.peek();
				if (activeInvocation instanceof MethodInvocation) {
					MethodInvocation methodInvocation = (MethodInvocation) activeInvocation;
					methodInvocation.getSelfMetrics().pause(startTickNs);
				}			
			}
		
			long currentThreadId = Thread.currentThread().getId();
			threadExecutedSqlStatement.putIfAbsent(currentThreadId, statementExecution);
		}
		
		return statementExecution;
	}
	
	public static void finishStatement() {
		long finishTickNs = TimerFactory.get().nanoTime();

		long currentThreadId = Thread.currentThread().getId();
		SqlStatementExecution statementExecution = threadExecutedSqlStatement.remove(currentThreadId);
		
		if (statementExecution != null) {
			statementExecution.getMetrics().finish(finishTickNs);
			if (callStack().isEmpty()) {
				trace.getCurrentThreadCallStack().add(statementExecution);
			}
			else {
				CompositeInvocation callStackTop = callStack().peek();
				if (callStackTop instanceof MethodInvocation) {
					MethodInvocation methodInvocation = (MethodInvocation) callStackTop;
					methodInvocation.getSelfMetrics().startOrContinue(finishTickNs);
				}
				callStackTop.add(statementExecution);
			}
		}
	}
	
	public static Invocation enterMethod(String methodName) {
		long startTickNs = TimerFactory.get().nanoTime();
		
		MethodInvocation result;
		
		Stack<CompositeInvocation> callStack = callStack();
		if (callStack.isEmpty()) {
			result = trace.getCurrentThreadCallStack().lookupOrCreate(methodName);
		}
		else {
			CompositeInvocation activeInvocation = callStack.peek();
			if (activeInvocation instanceof MethodInvocation) {
				MethodInvocation methodInvocation = (MethodInvocation) activeInvocation;
				methodInvocation.getSelfMetrics().pause(startTickNs);
			}
			result = activeInvocation.lookupOrCreate(methodName);
		}
		callStack.push(result);

		result.getMetrics().startOrContinue(startTickNs);
		result.getSelfMetrics().startOrContinue(startTickNs);
		return result;
	}

	public static void exitMethod() {
		finishInvocation();
	}

	private static void finishInvocation() {
		long finishTickNs = TimerFactory.get().nanoTime();
		
		Stack<CompositeInvocation> callStack = callStack();
		Invocation invocation = callStack.pop();
		invocation.getMetrics().finish(finishTickNs);
		if (invocation instanceof MethodInvocation) {
			MethodInvocation methodInvocation = (MethodInvocation) invocation;
			methodInvocation.getSelfMetrics().finish(finishTickNs);
		}
		if (!callStack.isEmpty()) {
			CompositeInvocation activeInvocation = callStack.peek();
			if (activeInvocation instanceof MethodInvocation) {
				MethodInvocation methodInvocation = (MethodInvocation) activeInvocation;
				methodInvocation.getSelfMetrics().startOrContinue(finishTickNs);
			}
		}
	}
	
	public static Transaction beginTransaction() {
		Transaction transaction = new Transaction();
		Stack<CompositeInvocation> callStack = callStack();
		if (callStack.isEmpty()) {
			ThreadCallStack threadCallStack = trace.getCurrentThreadCallStack();
			threadCallStack.add(transaction);						
		}
		callStack.push(transaction);
		
		return transaction;
	}
	
	public static void endTransaction(int transactionStatus) {
		Invocation top = callStack().pop();
		if (top instanceof Transaction) {
			Transaction transaction = (Transaction) top;
			transaction.setStatus(transactionStatus);
		}
		else {
			logger.warning("No transaction on top of callstack!");
		}
	}
	
	public static void save(Trace trace, String fileName)
			throws IOException, Exception {
		FileWriter fileWriter = new FileWriter(fileName);
		try {
			JAXBContext context = JAXBContext
					.newInstance(Trace.class);
			Marshaller marshaller = context.createMarshaller();
			marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT,
					Boolean.TRUE);

			marshaller.marshal(trace, fileWriter);
		} finally {
			fileWriter.close();
		}
	}

	public static Trace load(String fileName) throws IOException,
			Exception {
		FileReader fileReader = new FileReader(fileName);
		try {
			JAXBContext context = JAXBContext
					.newInstance(Trace.class); 
			Unmarshaller unmarshaller = context.createUnmarshaller();
			Trace result = (Trace) unmarshaller
					.unmarshal(fileReader);
			return result;
		} finally {
			fileReader.close();
		}
	}
	
	private static Stack<CompositeInvocation> callStack() {
		long currentThreadId = Thread.currentThread().getId();
		Stack<CompositeInvocation> callStack;
		if (!threadCallStack.containsKey(currentThreadId)) {
			callStack = new Stack<CompositeInvocation>();
			threadCallStack.putIfAbsent(currentThreadId, callStack);
		}
		else {
			callStack = threadCallStack.get(currentThreadId);
		}
		return callStack;
	}
}
